﻿
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using Microsoft.Xna.Framework.Graphics;

namespace Lucy.Integration.Wpf
{
    public enum XnaUsageMode
    {
        DrawAnimation,
        Custom,
    }

    public class XnaContent : HwndHost
    {
        public static readonly DependencyProperty XnaHandlerProperty =
            DependencyProperty.Register("XnaHandler", typeof(IXnaContentHandler), typeof(XnaContent), new UIPropertyMetadata(null));
        public static readonly DependencyProperty UsageModeProperty =
            DependencyProperty.Register("UsageMode", typeof(XnaUsageMode), typeof(XnaContent), new UIPropertyMetadata(XnaUsageMode.Custom));

        public IXnaContentHandler XnaHandler
        {
            get { return (IXnaContentHandler)GetValue(XnaHandlerProperty); }
            set { SetValue(XnaHandlerProperty, value); }
        }

        public XnaUsageMode UsageMode
        {
            get { return (XnaUsageMode)GetValue(UsageModeProperty); }
            set { SetValue(UsageModeProperty, value); }
        }
        
        private const string WindowClass = "GraphicsDeviceControlHostWindowClass";
        private IntPtr _hWnd;
        private GraphicsDeviceService _graphicsService;
        private bool _applicationHasFocus;
        private bool _mouseInWindow;
        private readonly HwndMouseState _mouseState = new HwndMouseState();
        private bool _isMouseCaptured;
        private int _capturedMouseX;
        private int _capturedMouseY;
        private int _capturedMouseClientX;
        private int _capturedMouseClientY;

        public event EventHandler<HwndMouseEventArgs> HwndLButtonDown;
        public event EventHandler<HwndMouseEventArgs> HwndLButtonUp;
        public event EventHandler<HwndMouseEventArgs> HwndLButtonDblClick;
        public event EventHandler<HwndMouseEventArgs> HwndRButtonDown;
        public event EventHandler<HwndMouseEventArgs> HwndRButtonUp;
        public event EventHandler<HwndMouseEventArgs> HwndRButtonDblClick;
        public event EventHandler<HwndMouseEventArgs> HwndMButtonDown;
        public event EventHandler<HwndMouseEventArgs> HwndMButtonUp;
        public event EventHandler<HwndMouseEventArgs> HwndMButtonDblClick;
        public event EventHandler<HwndMouseEventArgs> HwndX1ButtonDown;
        public event EventHandler<HwndMouseEventArgs> HwndX1ButtonUp;
        public event EventHandler<HwndMouseEventArgs> HwndX1ButtonDblClick;
        public event EventHandler<HwndMouseEventArgs> HwndX2ButtonDown;
        public event EventHandler<HwndMouseEventArgs> HwndX2ButtonUp;
        public event EventHandler<HwndMouseEventArgs> HwndX2ButtonDblClick;
        public event EventHandler<HwndMouseEventArgs> HwndMouseMove;
        public event EventHandler<HwndMouseEventArgs> HwndMouseEnter;
        public event EventHandler<HwndMouseEventArgs> HwndMouseLeave;

        public XnaContent()
        {
            Loaded += XnaWindowHostLoaded;
            SizeChanged += XnaWindowHostSizeChanged;
            Application.Current.Activated += CurrentActivated;
            Application.Current.Deactivated += CurrentDeactivated;
            CompositionTarget.Rendering += CompositionTargetRendering;
        }

        protected override void Dispose(bool disposing)
        {
            if (_graphicsService != null)
            {
                _graphicsService.Release(disposing);
                _graphicsService = null;
            }
            CompositionTarget.Rendering -= CompositionTargetRendering;
        }

        public new void CaptureMouse()
        {
            if (_isMouseCaptured)
                return;

            NativeMethods.ShowCursor(false);
            _isMouseCaptured = true;

            NativeMethods.POINT p = new NativeMethods.POINT();
            NativeMethods.GetCursorPos(ref p);
            _capturedMouseX = p.X;
            _capturedMouseY = p.Y;

            NativeMethods.ScreenToClient(_hWnd, ref p);
            _capturedMouseClientX = p.X;
            _capturedMouseClientY = p.Y;
        }

        public new void ReleaseMouseCapture()
        {
            if (!_isMouseCaptured)
                return;

            NativeMethods.ShowCursor(true);
            _isMouseCaptured = false;
        }

        void CompositionTargetRendering(object sender, EventArgs e)
        {
            if (_isMouseCaptured &&
                (int)_mouseState.Position.X != _capturedMouseX &&
                (int)_mouseState.Position.Y != _capturedMouseY)
            {
                NativeMethods.SetCursorPos(_capturedMouseX, _capturedMouseY);
                _mouseState.Position = _mouseState.PreviousPosition = new Point(_capturedMouseClientX, _capturedMouseClientY);
            }

            if (_graphicsService == null)
                return;

            int width = (int)ActualWidth;
            int height = (int)ActualHeight;

            if (width < 1 || height < 1)
                return;

            Viewport viewport = new Viewport(0, 0, width, height);
            _graphicsService.GraphicsDevice.Viewport = viewport;

            if (XnaHandler != null)
                XnaHandler.RenderXna(_graphicsService.GraphicsDevice);

            _graphicsService.GraphicsDevice.Present(viewport.Bounds, null, _hWnd);
        }

        void XnaWindowHostLoaded(object sender, RoutedEventArgs e)
        {
            if (_graphicsService == null)
            {
                _graphicsService = GraphicsDeviceService.AddRef(_hWnd, (int)ActualWidth, (int)ActualHeight);
                if (XnaHandler != null)
                    XnaHandler.LoadContent(_graphicsService.GraphicsDevice);
            }
        }

        void XnaWindowHostSizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (_graphicsService != null)
                _graphicsService.ResetDevice((int)ActualWidth, (int)ActualHeight);
        }

        void CurrentActivated(object sender, EventArgs e)
        {
            _applicationHasFocus = true;
        }

        void CurrentDeactivated(object sender, EventArgs e)
        {
            _applicationHasFocus = false;
            ResetMouseState();

            if (_mouseInWindow)
            {
                _mouseInWindow = false;
                if (HwndMouseLeave != null)
                    HwndMouseLeave(this, new HwndMouseEventArgs(_mouseState));
            }

            ReleaseMouseCapture();
        }

        private void ResetMouseState()
        {
            bool fireL = _mouseState.LeftButton == MouseButtonState.Pressed;
            bool fireM = _mouseState.MiddleButton == MouseButtonState.Pressed;
            bool fireR = _mouseState.RightButton == MouseButtonState.Pressed;
            bool fireX1 = _mouseState.X1Button == MouseButtonState.Pressed;
            bool fireX2 = _mouseState.X2Button == MouseButtonState.Pressed;

            _mouseState.LeftButton = MouseButtonState.Released;
            _mouseState.MiddleButton = MouseButtonState.Released;
            _mouseState.RightButton = MouseButtonState.Released;
            _mouseState.X1Button = MouseButtonState.Released;
            _mouseState.X2Button = MouseButtonState.Released;

            HwndMouseEventArgs args = new HwndMouseEventArgs(_mouseState);
            if (fireL && HwndLButtonUp != null)
                HwndLButtonUp(this, args);
            if (fireM && HwndMButtonUp != null)
                HwndMButtonUp(this, args);
            if (fireR && HwndRButtonUp != null)
                HwndRButtonUp(this, args);
            if (fireX1 && HwndX1ButtonUp != null)
                HwndX1ButtonUp(this, args);
            if (fireX2 && HwndX2ButtonUp != null)
                HwndX2ButtonUp(this, args);
            _mouseInWindow = false;
        }

        protected override HandleRef BuildWindowCore(HandleRef hwndParent)
        {
            _hWnd = CreateHostWindow(hwndParent.Handle);
            return new HandleRef(this, _hWnd);
        }

        protected override void DestroyWindowCore(HandleRef hwnd)
        {
            NativeMethods.DestroyWindow(hwnd.Handle);
            _hWnd = IntPtr.Zero;
        }

        private IntPtr CreateHostWindow(IntPtr hWndParent)
        {
            RegisterWindowClass();

            return NativeMethods.CreateWindowEx(0, WindowClass, string.Empty,
               NativeMethods.WS_CHILD | NativeMethods.WS_VISIBLE,
               0, 0, (int)Width, (int)Height, hWndParent, IntPtr.Zero, IntPtr.Zero, 0);
        }

        private void RegisterWindowClass()
        {
            NativeMethods.WNDCLASSEX wndClass = new NativeMethods.WNDCLASSEX();
            wndClass.cbSize = (uint)Marshal.SizeOf(wndClass);
            wndClass.hInstance = NativeMethods.GetModuleHandle(null);
            wndClass.lpfnWndProc = NativeMethods.DefaultWindowProc;
            wndClass.lpszClassName = WindowClass;
            wndClass.hCursor = NativeMethods.LoadCursor(IntPtr.Zero, NativeMethods.IDC_ARROW);
            NativeMethods.RegisterClassEx(ref wndClass);
        }

        protected override IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            switch (msg)
            {
                case NativeMethods.WM_LBUTTONDOWN:
                    _mouseState.LeftButton = MouseButtonState.Pressed;
                    if (HwndLButtonDown != null)
                        HwndLButtonDown(this, new HwndMouseEventArgs(_mouseState));
                    break;
                case NativeMethods.WM_LBUTTONUP:
                    _mouseState.LeftButton = MouseButtonState.Released;
                    if (HwndLButtonUp != null)
                        HwndLButtonUp(this, new HwndMouseEventArgs(_mouseState));
                    break;
                case NativeMethods.WM_LBUTTONDBLCLK:
                    if (HwndLButtonDblClick != null)
                        HwndLButtonDblClick(this, new HwndMouseEventArgs(_mouseState, MouseButton.Left));
                    break;
                case NativeMethods.WM_RBUTTONDOWN:
                    _mouseState.RightButton = MouseButtonState.Pressed;
                    if (HwndRButtonDown != null)
                        HwndRButtonDown(this, new HwndMouseEventArgs(_mouseState));
                    break;
                case NativeMethods.WM_RBUTTONUP:
                    _mouseState.RightButton = MouseButtonState.Released;
                    if (HwndRButtonUp != null)
                        HwndRButtonUp(this, new HwndMouseEventArgs(_mouseState));
                    break;
                case NativeMethods.WM_RBUTTONDBLCLK:
                    if (HwndRButtonDblClick != null)
                        HwndRButtonDblClick(this, new HwndMouseEventArgs(_mouseState, MouseButton.Right));
                    break;
                case NativeMethods.WM_MBUTTONDOWN:
                    _mouseState.MiddleButton = MouseButtonState.Pressed;
                    if (HwndMButtonDown != null)
                        HwndMButtonDown(this, new HwndMouseEventArgs(_mouseState));
                    break;
                case NativeMethods.WM_MBUTTONUP:
                    _mouseState.MiddleButton = MouseButtonState.Released;
                    if (HwndMButtonUp != null)
                        HwndMButtonUp(this, new HwndMouseEventArgs(_mouseState));
                    break;
                case NativeMethods.WM_MBUTTONDBLCLK:
                    if (HwndMButtonDblClick != null)
                        HwndMButtonDblClick(this, new HwndMouseEventArgs(_mouseState, MouseButton.Middle));
                    break;
                case NativeMethods.WM_XBUTTONDOWN:
                    if (((int)wParam & NativeMethods.MK_XBUTTON1) != 0)
                    {
                        _mouseState.X1Button = MouseButtonState.Pressed;
                        if (HwndX1ButtonDown != null)
                            HwndX1ButtonDown(this, new HwndMouseEventArgs(_mouseState));
                    }
                    else if (((int)wParam & NativeMethods.MK_XBUTTON2) != 0)
                    {
                        _mouseState.X2Button = MouseButtonState.Pressed;
                        if (HwndX2ButtonDown != null)
                            HwndX2ButtonDown(this, new HwndMouseEventArgs(_mouseState));
                    }
                    break;
                case NativeMethods.WM_XBUTTONUP:
                    if (((int)wParam & NativeMethods.MK_XBUTTON1) != 0)
                    {
                        _mouseState.X1Button = MouseButtonState.Released;
                        if (HwndX1ButtonUp != null)
                            HwndX1ButtonUp(this, new HwndMouseEventArgs(_mouseState));
                    }
                    else if (((int)wParam & NativeMethods.MK_XBUTTON2) != 0)
                    {
                        _mouseState.X2Button = MouseButtonState.Released;
                        if (HwndX2ButtonUp != null)
                            HwndX2ButtonUp(this, new HwndMouseEventArgs(_mouseState));
                    }
                    break;
                case NativeMethods.WM_XBUTTONDBLCLK:
                    if (((int)wParam & NativeMethods.MK_XBUTTON1) != 0)
                    {
                        if (HwndX1ButtonDblClick != null)
                            HwndX1ButtonDblClick(this, new HwndMouseEventArgs(_mouseState, MouseButton.XButton1));
                    }
                    else if (((int)wParam & NativeMethods.MK_XBUTTON2) != 0)
                    {
                        if (HwndX2ButtonDblClick != null)
                            HwndX2ButtonDblClick(this, new HwndMouseEventArgs(_mouseState, MouseButton.XButton2));
                    }
                    break;
                case NativeMethods.WM_MOUSEMOVE:
                    if (!_applicationHasFocus)
                        break;

                    _mouseState.PreviousPosition = _mouseState.Position;
                    _mouseState.Position = new Point(
                        NativeMethods.GetXLParam((int)lParam),
                        NativeMethods.GetYLParam((int)lParam));

                    if (!_mouseInWindow)
                    {
                        _mouseInWindow = true;

                        _mouseState.PreviousPosition = _mouseState.Position;

                        if (HwndMouseEnter != null)
                            HwndMouseEnter(this, new HwndMouseEventArgs(_mouseState));

                        NativeMethods.TRACKMOUSEEVENT tme = new NativeMethods.TRACKMOUSEEVENT();
                        tme.cbSize = Marshal.SizeOf(typeof(NativeMethods.TRACKMOUSEEVENT));
                        tme.dwFlags = NativeMethods.TME_LEAVE;
                        tme.hWnd = hwnd;
                        NativeMethods.TrackMouseEvent(ref tme);
                    }

                    if (_mouseState.Position != _mouseState.PreviousPosition)
                    {
                        if (HwndMouseMove != null)
                            HwndMouseMove(this, new HwndMouseEventArgs(_mouseState));
                    }

                    break;
                case NativeMethods.WM_MOUSELEAVE:

                    if (_isMouseCaptured)
                        break;

                    ResetMouseState();

                    if (HwndMouseLeave != null)
                        HwndMouseLeave(this, new HwndMouseEventArgs(_mouseState));
                    break;
            }

            return base.WndProc(hwnd, msg, wParam, lParam, ref handled);
        }

    }
}
